import sys
import os
import re

thisDir = os.path.split(os.path.realpath(__file__))[0]
sys.path.append(os.path.join(thisDir, ".."))
sys.path.append(os.path.join(thisDir, "..", "..", "..", "server", "ps"))

import PUConst
import PUGenerator
import SessionManager

import logging
logger = logging.getLogger(__name__)

class PanelGenerator(PUGenerator.PUPanelGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "div")

class TextGenerator(PUGenerator.PUTextGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "p", ui[PUConst.UiPropText])

class SeparatorGenerator(PUGenerator.PUSeparatorGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "hr")

class SpinPageGenerator(PUGenerator.PUSpinPageGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        current = self._pu.GetConfigure(uid)
        if current is None:
            logger.error(f"Current spin page is not defined for {uid}")
            return self._pu.GenerateEmpty()
        elif isinstance(current, str):
            # Current is uid, so the SubUI must be list of string or dict
            if current not in ui[PUConst.UiPropSubUI]:
                logger.error(f"Specified uid is not in spin page: {current}")
                return self._pu.GenerateEmpty()
            return self._pu.Generate(current)
        elif isinstance(current, int):
            # Current is index, so the SubUI must be list
            if not isinstance(ui[PUConst.UiPropSubUI], list):
                logger.error(f"Spin pages is not defined as list to full fill specified index: {current}")
                return self._pu.GenerateEmpty()
            if current < 0 or current >= len(ui[PUConst.UiPropSubUI]):
                logger.error(f"Specified index of spin pages is not valid: {current}")
                return self._pu.GenerateEmpty()
            return self._pu.Generate(ui[PUConst.UiPropSubUI][current])
        logger.error("Spin pages unknown error")
        return self._pu.GenerateEmpty()

class LayoutHorizontalGenerator(PUGenerator.PULayoutHorizontalGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        columnDef = ""
        subUIs = ui[PUConst.UiPropSubUI] if PUConst.UiPropSubUI in ui else []
        i = 0
        while i < len(subUIs):
            columnDef += " auto"
            i += 1
        styleMap = { "display": "grid", "grid-template-columns": columnDef } if columnDef else { "display": "grid" }
        return self._pu.GenerateElement(self, uid, ui, "div", None, {}, styleMap)

class LayoutVerticalGenerator(PUGenerator.PULayoutVerticalGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "div", None, {}, { "display": "grid" })

class LayoutGridGenerator(PUGenerator.PULayoutGridGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        columnDef = ""
        i = 0
        while i < int(ui[PUConst.UiPropColumn]):
            columnDef += " auto"
            i += 1
        styleMap = { "display": "grid", "grid-template-columns": columnDef } if columnDef else { "display": "grid" }
        return self._pu.GenerateElement(self, uid, ui, "div", None, {}, styleMap)

class InputGenerator(PUGenerator.PUInputGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "input", None, { "type": "text" })

class PasswordGenerator(PUGenerator.PUPasswordGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "input", None, { "type": "password" })

class ButtonGenerator(PUGenerator.PUButtonGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        attrMap = { "type": "submit" }
        if PUConst.UiPropText in ui:
            attrMap["value"] = ui[PUConst.UiPropText]
        return self._pu.GenerateElement(self, uid, ui, "input", None, attrMap)

class LinkGenerator(PUGenerator.PULinkGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        url = ui[PUConst.UiPropURL]
        index = 0
        while index >= 0:
            index = url.find("{", index)
            if index < 0:
                # End of search
                break
            # Make sure it is not {{
            if index < len(url) - 1 and url[index + 1] == "{":
                index += 2
                continue
            preIndex = index
            index = url.find("}", index)
            if index < 0:
                # Not {} pair
                break
            # Make sure it is not }}
            if index < len(url) - 1 and url[index + 1] == "}":
                index = preIndex + 2
                continue
            # Insert $ before {
            url = url[:preIndex] + "$" + url[preIndex:]
            # Move to next pair
            index += 2
        script = f"<script>document.getElementById('{uid}').href = `{url}`;</script>"
        self._pu.AddPostScript(uid, script)
        return self._pu.GenerateElement(self, uid, ui, "a", ui[PUConst.UiPropText])

class TabGenerator(PUGenerator.PUTabGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        borderStyle = "solid"
        borderWidth = "1px"
        bgColor = "white"
        tabBody = ""
        # SubUI for Tab control must be even number
        # The first is tab header and the second is tab content
        numSubUI = len(ui[PUConst.UiPropSubUI])
        if numSubUI > 0 and numSubUI % 2 == 0:
            headerBody = ""
            contentBody = ""
            index = 0
            for subUI in self.GenerateSubUI(uid, ui):
                if index % 2 == 0:
                    # It is header
                    border = f"border-bottom:{borderWidth} {borderStyle} {bgColor}; border-top:{borderWidth} {borderStyle}; border-left:{borderWidth} {borderStyle}; border-right:{borderWidth} {borderStyle}"
                    click = f"OnTabClicked(\"{uid}\", {int(index / 2)}, {int(numSubUI / 2)})"
                    style = f"float:left; list-style:none; margin-bottom:-{borderWidth}; background-color: {bgColor}; {border if index == 0 else ''}"
                    headerBody += f"<li id='{uid + '_tab_header_'}{int(index / 2)}' style='{style}' onclick='{click}'>"
                    headerBody += subUI
                    headerBody += f"</li>"
                else:
                    # It is content
                    style = f"display:{'block' if index == 1 else 'none'}"
                    contentBody += f"<div id='{uid + '_tab_content_'}{int((index - 1) / 2)}' style='{style}'>"
                    contentBody += subUI
                    contentBody += f"</div>"
                index += 1
            tabBody += f"<ul id='{uid + '_tab_header'}'>"
            tabBody += headerBody
            tabBody += f"</ul>"
            tabBody += f"<div id='{uid + '_tab_content'}' style='clear:both; background-color:{bgColor}; border-top:{borderWidth} {borderStyle}'>"
            tabBody += contentBody
            tabBody += f"</div>"
        else:
            logger.warning(f"Invalid sub UI number for tab control: {uid}")
        return self._pu.GenerateElement(self, uid, ui, "div", tabBody)

class TableGenerator(PUGenerator.PUTableGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        numColumn = int(ui[PUConst.UiPropColumn])
        hasHeader = PUConst.UiPropHasHeader in ui and ui[PUConst.UiPropHasHeader]
        subUIs = []
        for subUI in self.GenerateSubUI(uid, ui):
            subUIs.append(subUI)
        tableBody = ""
        index = 0
        while index < len(subUIs):
            createHeader = index < numColumn and hasHeader
            tableBody += "<tr>"
            for column in range(numColumn):
                tableBody += "<th>" if createHeader else "<td>"
                tableBody += subUIs[index] if index < len(subUIs) else self._pu.GenerateElement()
                tableBody += "</th>" if createHeader else "</td>"
                index += 1
            tableBody += "</tr>"
        attrMap = { "hasHeader": "true" if hasHeader else "false" }
        return self._pu.GenerateElement(self, uid, ui, "table", tableBody, attrMap)

class DatetimeGenerator(PUGenerator.PUDatetimeGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "input", None, { "type": "date" })

class ComboBoxGenerator(PUGenerator.PUComboBoxGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "select")

class ComboListGenerator(PUGenerator.PUComboListGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        return self._pu.GenerateElement(self, uid, ui, "option", ui[PUConst.UiPropText], { "value": ui[PUConst.UiPropText] })

class MapGenerator(PUGenerator.PUMapGenerator):
    def __init__(self, generator):
        super().__init__(generator)
    def Generate(self, uid, ui):
        msKey = ui[PUConst.UiPropMapCredentical]
        initLocation = ["31.230369567871094", "121.47370147705078"] # Shanghai
        script = ""
        script += f"<script type='text/javascript'>"
        script += f"    function loadMap{uid}() {{"
        script += f"        var map = new Microsoft.Maps.Map(document.getElementById('{uid}'), {{ center: new Microsoft.Maps.Location({initLocation[0]}, {initLocation[1]}) }});"
        script += f"    }}"
        script += f"</script>"
        script += f"<script type='text/javascript' src='https://www.bing.com/api/maps/mapcontrol?key={msKey}&callback=loadMap{uid}' async defer></script>"
        self._pu.AddPostScript(uid, script)
        return self._pu.GenerateElement(self, uid, ui, "div")

class PUHtml(PUGenerator.PUGenerator):
    def __init__(self, uiConfig):
        super().__init__(uiConfig)
        self._clear()

    ### Utilities ###

    # This is where each element is generated.
    # Each generator must call this function to generate UI for HTML
    def GenerateElement(self, generator, uid, ui, tag, value = None, attrMap = {}, styleMap = {}):
        if tag == "hr":
            return "<hr>"
        self.__generateProp(uid, ui)
        attrMap["id"] = uid
        element = f"<{tag + self.__generateAttr(uid, ui, attrMap, styleMap)}>"
        if value:
            element += value
        else:
            for subUI in generator.GenerateSubUI(uid, ui):
                element += subUI
        element += f"</{tag}>"
        prefixScript = self.__prefixScripts[uid] if uid in self.__prefixScripts else ""
        postScript = self.__postScripts[uid] if uid in self.__postScripts else ""
        return prefixScript + element + postScript

    def AddPrefixScript(self, uid, script):
        if uid in self.__prefixScripts:
            self.__prefixScripts[uid] += script
        else:
            self.__prefixScripts[uid] = script

    def AddPostScript(self, uid, script):
        if uid in self.__postScripts:
            self.__postScripts[uid] += script
        else:
            self.__postScripts[uid] = script

    ### Override ###

    def _clear(self):
        super()._clear()
        self.__prefixScripts = {}
        self.__postScripts = {}
        self.__properties = {}

    def _preGenerate(self):
        return f"<!DOCTYPE html><html>{self.__generateUtilityScript()}<body>"

    def _postGenerate(self):
        return "</body></html>"

    def _createPanelGenerator(self):
        return PanelGenerator(self)

    def _createTextGenerator(self):
        return TextGenerator(self)

    def _createSeparatorGenerator(self):
        return SeparatorGenerator(self)

    def _createSpinPageGenerator(self):
        return SpinPageGenerator(self)

    def _createLayoutHorizontalGenerator(self):
        return LayoutHorizontalGenerator(self)

    def _createLayoutVerticalGenerator(self):
        return LayoutVerticalGenerator(self)

    def _createLayoutGridGenerator(self):
        return LayoutGridGenerator(self)

    def _createInputGenerator(self):
        return InputGenerator(self)

    def _createPasswordGenerator(self):
        return PasswordGenerator(self)

    def _createButtonGenerator(self):
        return ButtonGenerator(self)

    def _createLinkGenerator(self):
        return LinkGenerator(self)

    def _createTabGenerator(self):
        return TabGenerator(self)

    def _createTableGenerator(self):
        return TableGenerator(self)

    def _createDatetimeGenerator(self):
        return DatetimeGenerator(self)

    def _createComboBoxGenerator(self):
        return ComboBoxGenerator(self)

    def _createComboListGenerator(self):
        return ComboListGenerator(self)

    def _createMapGenerator(self):
        return MapGenerator(self)

    ### Private ###

    def __generateProp(self, uid, ui):
        self.__properties[uid] = {}
        if PUConst.UiPropVisible in ui and not ui[PUConst.UiPropVisible]:
            self.__properties[uid]["display"] = "none"
        if PUConst.UiPropColor in ui:
            self.__properties[uid]["color"] = self.__getColor(ui[PUConst.UiPropColor])
        if PUConst.UiPropApiTrigger in ui:
            for action, function in self.__generateAction(uid, ui):
                self.__properties[uid][action] = function

    def __generateStyle(self, uid, ui, styleMap):
        styles = []
        for key, value in self.__properties[uid].items():
            if key == "display" or key == "color":
                styles.append(f"{key}:{value}")
        for key in styleMap:
            styles.append(f"{key}:{styleMap[key]}")
        return ";".join(styles) if len(styles) else None

    def __generateAttr(self, uid, ui, attrMap, styleMap):
        attrs = ""
        for key in self.__properties[uid]:
            if key == "onclick":
                attrs += f" {key}=\"{self.__properties[uid][key]}\""
        styleAttr = self.__generateStyle(uid, ui, styleMap)
        if styleAttr:
            attrs += f" style=\"{styleAttr}\""
        for key in attrMap:
            attrs += f" {key}=\"{attrMap[key]}\""
        return attrs

    def __generateAction(self, uid, ui):
        for trigger in ui[PUConst.UiPropApiTrigger]:
            action = self.__getAction(trigger[PUConst.UiApiTriggerAction])
            function = trigger[PUConst.UiApi] + action + "()"
            self.__generateTriggerScript(uid, trigger, action)
            yield action, function

    def __generateTriggerScript(self, uid, trigger, action):
        script = ""
        script += f"<script>"
        script += f"function {trigger[PUConst.UiApi] + action}() {{"
        script += self.__generateFunctionCall(trigger)
        script += f"}}"
        script += f"</script>"
        self.AddPrefixScript(uid, script)

    def __generateFunctionCall(self, trigger):
        api = trigger[PUConst.UiApi]
        if re.match("^aid\\w+", api):
            # It's API call
            return self.__generateApiFunctionCall(trigger)
        return self._pu.GenerateEmpty()

    def __generateApiFunctionCall(self, trigger):
        api = SessionManager.SessionManager().GetProperty("api")
        aid = trigger[PUConst.UiApi]

        # Handle Parameters
        params = []
        variables = []
        if PUConst.UiApiParams in trigger:
            index = 0
            for param in trigger[PUConst.UiApiParams]:
                if param[PUConst.UiApiDataType] == PUConst.UiApiTypeUID:
                    variables.append(f"let param{index} = GetValueByUID(\"{param[PUConst.UiApiDataValue]}\");")
                if param[PUConst.UiApiDataType] == PUConst.UiApiTypeFunc:
                    variables.append(f"let param{index} = {param[PUConst.UiApiDataValue]};")
                params.append(f"${{param{index}}}")
                index += 1
        apiString = api.GenerateApi(aid, params)
        if not apiString:
            logger.error(f"Generate api failed for api: {aid}")
            return ""

        # Handler Results
        successfulCalls = []
        failedCalls = []
        authenticationCalls = []
        exceptionCalls = []
        resultHandlers = []
        if PUConst.UiApiResults in trigger:
            if PUConst.UiApiResultSuccessful in trigger[PUConst.UiApiResults]:
                successfulCalls = [f"\"{func}\"" for func in trigger[PUConst.UiApiResults][PUConst.UiApiResultSuccessful]]
            if PUConst.UiApiResultFailed in trigger[PUConst.UiApiResults]:
                failedCalls = [f"\"{func}\"" for func in trigger[PUConst.UiApiResults][PUConst.UiApiResultFailed]]
            if PUConst.UiApiResultInvalidToken in trigger[PUConst.UiApiResults]:
                authenticationCalls = [f"\"{func}\"" for func in trigger[PUConst.UiApiResults][PUConst.UiApiResultInvalidToken]]
            if PUConst.UiApiResultException in trigger[PUConst.UiApiResults]:
                exceptionCalls = [f"\"{func}\"" for func in trigger[PUConst.UiApiResults][PUConst.UiApiResultException]]
        resultFormater = "ToObj" if api.IsResultDefined(aid) else "ToString"
        resultHandlers.append(f"let successfulCalls = [{','.join(successfulCalls)}];")
        resultHandlers.append(f"let failedCalls = [{','.join(failedCalls)}];")
        resultHandlers.append(f"let authenticationCalls = [{','.join(authenticationCalls)}];")
        resultHandlers.append(f"let exceptionCalls = [{','.join(exceptionCalls)}];")
        resultHandlers.append(f"let resultFormater = {resultFormater};")

        return " ".join(variables) + " ".join(resultHandlers) + f"CallAPI(`{apiString}`, successfulCalls, failedCalls, authenticationCalls, exceptionCalls, resultFormater);"

    def __generateUtilityScript(self):
        script = ""
        script += "<script>"
        script += '''
            // Utilities
            function ToObj(text)
            {
                return eval("(" + text + ")");
            }
            function ToString(text)
            {
                return text.toString();
            }
            function ResolveFuncCallVariables(funcString, index, level)
            {
                let result = "";
                let i = index;
                let matched = level == 0 ? true : false;
                for (; i < funcString.length; ++i) {
                    if (funcString[i] == "{") {
                        // Make sure it is no {{
                        if (((i + 1) < funcString.length) && (funcString[i + 1] == "{")) {
                            result += "{{";
                            ++i;
                            continue;
                        }
                        let resolveResult = ResolveFuncCallVariables(funcString, i + 1, level + 1);
                        result += resolveResult[0];
                        i = resolveResult[1];
                        continue;
                    }
                    if (funcString[i] == "}") {
                        // Make sure it is no }}
                        if (((i + 1) < funcString.length) && (funcString[i + 1] == "}")) {
                            result += "}}";
                            ++i;
                            continue;
                        }
                        if (level != 0) {
                            matched = true;
                            break;
                        } else {
                            console.log("Mismatch for {}, only } is there");
                        }
                    }
                    result += funcString[i];
                }
                if (matched) {
                    if (level != 0) {
                        result = eval(result);
                    }
                } else {
                    console.log("Mismatch for {}");
                }
                return [result, i]
            }
            function OnAPIResult(funcs, ...args)
            {
                funcs.forEach((func) => {
                    let funcString = func;
                    for (let i = 0; i < args.length; ++i) {
                        if (funcString.search("apiparamholder") >= 0) {
                            funcString = funcString.replace("apiparamholder", "args[" + i.toString() + "]");
                        } else {
                            break;
                        }
                    }
                    if (funcString.search("apiparamholder") >= 0) {
                        console.log("OnAPIResult failed because of unresolved param holder: " + funcString);
                    } else {
                        funcString = ResolveFuncCallVariables(funcString, 0, 0)[0];
                        funcString = funcString.replace(/{{/g, "{");
                        funcString = funcString.replace(/}}/g, "}");
                        console.log("OnAPIResult: " + funcString);
                        eval(funcString);
                    }
                });
            }
            function CallAPI(api, onSuccessful, onFailed, onAuthentication, onException, resultFormater)
            {
                console.log("CallAPI: " + api);
                let host = location.host;
                let status = "";
                fetch("https://" + host + "/" + api)
                    .then(response => {
                        status = response.status;
                        return response.text();
                    })
                    .then(txt => {
                        if (status == "200") {
                            console.log("On API result successful");
                            OnAPIResult(onSuccessful, resultFormater(txt));
                        } else if (status == "401") {
                            console.log("On API result invalid authentication");
                            OnAPIResult(onAuthentication, txt);
                        } else {
                            console.log("On API result failed");
                            OnAPIResult(onFailed, txt);
                        }
                    })
                    .catch(error => {
                        console.log("On API result exception");
                        console.log(error);
                        OnAPIResult(onException, error);
                    });
            }
            function OnTabClicked(uid, currentTab, tabSize)
            {
                for (let i = 0; i < tabSize; ++i) {
                    let display = i == currentTab ? "block" : "none";
                    SetUICSS(uid + "_tab_content_" + i.toString(), {"display": display});
                    let borderStyle = "solid";
                    let borderWidth = "1px";
                    let bgColor = "white";
                    let uidHeader = uid + "_tab_header_" + i.toString();
                    let css = {};
                    if (i == currentTab) {
                        let style = borderWidth + " " + borderStyle;
                        let bottomStyle = style + " " + bgColor;
                        css["border-bottom"] = bottomStyle;
                        css["border-top"] = style;
                        css["border-left"] = style;
                        css["border-right"] = style;
                    } else {
                        css["border"] = "none";
                    }
                    SetUICSS(uidHeader, css);
                }
            }
            function SetUICSS(uid, css)
            {
                let element = document.getElementById(uid);
                if (element != null) {
                    for (let key in css) {
                        element.style[key] = css[key];
                    }
                } else {
                    console.log("Failed to find element: " + uid);
                }
            }

            // APIs
            function GetSession()
            {
                let name = "SESSION=";
                let all = document.cookie.split(';');
                for (let i = 0; i < all.length; i++) {
                    let item = all[i].trim();
                    if (item.indexOf(name) == 0) {
                        return item.substring(name.length, item.length);
                    }
                }
                return "";
            }
            function SetSession(val)
            {
                if (val.hasOwnProperty('session')) {
                    let session = val['session'];
                    document.cookie = "SESSION="+session+";";
                }
            }
            function GetValueByUID(uid)
            {
                let element = document.getElementById(uid);
                if (element == null) {
                    return "";
                }
                return element.value;
            }
            function OpenPage(url)
            {
                window.location.href = url;
            }
            function SetVisible(pageId)
            {
                // TODO
                // To implement set visible
                console.log(pageId);
            }
            function ShowMessage(msg)
            {
                alert(msg);
            }
            function GetHost()
            {
                return document.location.host;
            }
            function GetClient()
            {
                return "clienthtml";
            }
            function FillTableWithAPIResult(uid, result)
            {
                let element = document.getElementById(uid);
                if (element == null) {
                    console.log("Failed to find element: " + uid);
                    return;
                }
                if (element.tagName.toLowerCase() != "table") {
                    console.log("It's not table: " + uid);
                    return;
                }
                let firstRow = element.getAttribute("hasHeader").toLowerCase() == "true" ? 1 : 0;
                for (let i = element.rows.length - 1; i >= firstRow; --i) {
                    element.deleteRow(i);
                }
                for (let i = 0; i < result.length; ++i) {
                    let row = element.insertRow(firstRow + i);
                    for (let j = 0; j < result[i].length; ++j) {
                        let cell = row.insertCell(j);
                        cell.innerHTML = result[i][j];
                    }
                }
            }
        '''
        script += "</script>"
        return script

    def __getColor(self, puColor):
        if puColor == PUConst.Red:
            return "red"
        logger.warning(f"Un matched color: {puColor}")
        return "red"

    def __getAction(self, action):
        if action == PUConst.UiApiActionClick:
            return "onclick"
